homework 5, version 0

13.5 μs

Submission by: Ian Weaver (hahvard@mit.edu)

6.1 ms
5.4 μs
8.0 ms

Homework 5: Epidemic modeling II

18.S191, fall 2020

This notebook contains built-in, live answer checks! In some exercises you will see a coloured box, which runs a test case on your code, and provides feedback based on the result. Simply edit the code, run it, and the check runs again.

For MIT students: there will also be some additional (secret) test cases that will be run as part of the grading process, and we will look at your notebook and write comments.

Feel free to ask questions!

18.6 μs
student
53.0 ns

Let's create a package environment:

6.2 μs
48.0 ms
12.1 s

In this problem set, we will look at a simple spatial agent-based epidemic model: agents can interact only with other agents that are nearby. (In the previous homework any agent could interact with any other, which is not realistic.)

A simple approach is to use discrete space: each agent lives in one cell of a square grid. For simplicity we will allow no more than one agent in each cell, but this requires some care to design the rules of the model to respect this.

We will adapt some functionality from the previous homework. You should copy and paste your code from that homework into this notebook.

9.6 μs




68.0 ns

Exercise 1: Wandering at random in 2D

In this exercise we will implement a random walk on a 2D lattice (grid). At each time step, a walker jumps to a neighbouring position at random (i.e. chosen with uniform probability from the available adjacent positions).

9.2 μs

Exercise 1.1

We define a struct type Coordinate that contains integers x and y.

6.9 μs
3.9 ms

👉 Construct a Coordinate located at the origin.

6.3 μs
origin
4.3 ms

Got it!

Keep it up!

3.8 ms

👉 Write a function make_tuple that takes an object of type Coordinate and returns the corresponding tuple (x, y). Boring, but useful later!

4.7 μs
make_tuple (generic function with 1 method)
23.7 μs

Got it!

You got the right answer!

7.1 ms

Exercise 1.2

In Julia, operations like + and * are just functions, and they are treated like any other function in the language. The only special property you can use the infix notation: you can write

1 + 2

instead of

+(1, 2)

(There are lots of special 'infixable' function names that you can use for your own functions!)

When you call it with the prefix notation, it becomes clear that it really is 'just another function', with lots of predefined methods.

10.8 μs
3
66.0 ns
+ (generic function with 199 methods)
77.0 ns

Extending + in the wild

Because it is a function, we can add our own methods to it! This feature is super useful in general languages like Julia and Python, because it lets you use familiar syntax (a + b*c) on objects that are not necessarily numbers!

One example we've see before is the RGB type in Homework 1. You are able to do:

0.5 * RGB(0.1, 0.7, 0.6)

to multiply each color channel by 0.5. This is possible because Images.jl wrote a method:

*(::Real, ::AbstractRGB)::AbstractRGB

👉 Implement addition on two Coordinate structs by adding a method to Base.:+

3.4 μs
154 μs
8.4 μs

Pluto has some trouble here, you need to manually re-run the cell above!

7.2 μs

Got it!

Yay ❤

30.3 ms

Exercise 1.3

In our model, agents will be able to walk in 4 directions: up, down, left and right. We can define these directions as Coordinates.

5.5 μs
possible_moves
19.9 ms

👉 rand(possible_moves) gives a random possible move. Add this to the coordinate Coordinate(4,5) and see that it moves to a valid neighbor.

7.4 μs
5.0 ms

We are able to make a Coordinate perform one random step, by adding a move to it. Great!

👉 Write a function trajectory that calculates a trajectory of a Wanderer w when performing n steps., i.e. the sequence of positions that the walker finds itself in.

Possible steps:

  • Use rand(possible_moves, n) to generate a vector of n random moves. Each possible move will be equally likely.

  • To compute the trajectory you can use either of the following two approaches:

    1. 🆒 Use the function accumulate (see the live docs for accumulate). Use + as the function passed to accumulate and the w as the starting value (init keyword argument).

    2. Use a for loop calling +.

13.5 μs
trajectory (generic function with 1 method)
38.3 μs
69.6 ms
test_trajectory
14.6 μs
1.2 ms

Got it!

Awesome!

6.2 ms
plot_trajectory! (generic function with 1 method)
52.9 μs
2.5 ms

Exercise 1.4

👉 Plot 10 trajectories of length 1000 on a single figure, all starting at the origin. Use the function plot_trajectory! as demonstrated above.

Remember from last week that you can compose plots like this:

let
    # Create a new plot with aspect ratio 1:1
    p = plot(ratio=1)

    plot_trajectory!(p, test_trajectory)      # plot one trajectory
    plot_trajectory!(p, another_trajectory)   # plot the second one
    ...

    p
end
6.8 μs
11.0 ms

Exercise 1.5

Agents live in a box of side length 2L, centered at the origin. We need to decide (i.e. model) what happens when they reach the walls of the box (boundaries), in other words what kind of boundary conditions to use.

One relatively simple boundary condition is a collision boundary:

Each wall of the box is a wall, modelled using "collision": if the walker tries to jump beyond the wall, it ends up at the position inside the box that is closest to the goal.

👉 Write a function collide_boundary which takes a Coordinate c and a size L, and returns a new coordinate that lies inside the box (i.e. [L,L]×[L,L]), but is closest to c. This is similar to extend_mat from Homework 1.

8.8 μs
collide_boundary (generic function with 1 method)
24.9 μs
5.3 ms

Exercise 1.6

👉 Implement a 3-argument method of trajectory where the third argument is a size. The trajectory returned should be within the boundary (use collide_boundary from above). You can still use accumulate with an anonymous function that makes a move and then reflects the resulting coordinate, or use a for loop.

5.1 μs
trajectory (generic function with 2 methods)
12.9 ms
trajectory2 (generic function with 1 method)
45.9 μs
64.8 ms
  557.897 ns (32 allocations: 1.41 KiB)
3.8 s
  576.151 ns (32 allocations: 1.50 KiB)
3.9 s
37.2 ms




81.0 ns

Exercise 2: Wanderering Agents

In this exercise we will create Agents which have a location as well as some infection state information.

Let's define a type Agent. Agent contains a position (of type Coordinate), as well as a status of type InfectionStatus (as in Homework 4).)

(For simplicity we will not use a num_infected field, but feel free to do so!)

9.3 μs
12.0 ms
1.9 ms
4.7 ms

Exercise 2.1

👉 Write a function initialize that takes parameters N and L, where N is the number of agents abd 2L is the side length of the square box where the agents live.

It returns a Vector of N randomly generated Agents. Their coordinates are randomly sampled in the [L,L]×[L,L] box, and the agents are all susceptible, except one, chosen at random, which is infectious.

6.5 μs
initialize (generic function with 1 method)
85.1 μs
32.4 μs

Got it!

Fantastic!

413 ms
color (generic function with 1 method)
25.5 μs
position (generic function with 1 method)
19.5 μs
color (generic function with 2 methods)
21.9 μs

Exercise 2.2

👉 Write a function visualize that takes in a collection of agents as argument, and the box size L. It should plot a point for each agent at its location, coloured according to its status.

You can use the keyword argument c=color.(agents) inside your call to the plotting function make the point colors correspond to the infection statuses. Don't forget to use ratio=1.

7.5 μs
visualize (generic function with 1 method)
28.9 μs
134 ms

Exercise 3: Spatial epidemic model – Dynamics

Last week we wrote a function interact! that takes two agents, agent and source, and an infection of type InfectionRecovery, which models the interaction between two agent, and possibly modifies agent with a new status.

This week, we define a new infection type, CollisionInfectionRecovery, and a new method that is the same as last week, except it only infects agent if agents.position==source.position.

8.8 μs
7.7 μs
2.0 ms

Write a function interact! that takes two Agents and a CollisionInfectionRecovery, and:

  • If the agents are at the same spot, causes a susceptible agent to communicate the desease from an infectious one with the correct probability.

  • if the first agent is infectious, it recovers with some probability

12.6 μs
bernoulli (generic function with 1 method)
19.5 μs
is_infected (generic function with 1 method)
17.6 μs
is_susceptible (generic function with 1 method)
22.0 μs
set_status! (generic function with 1 method)
17.9 μs
interact! (generic function with 1 method)
38.0 μs

Exercise 3.1

Your turn!

👉 Write a function step! that takes a vector of Agents, a box size L and an infection. This that does one step of the dynamics on a vector of agents.

  • Choose an Agent source at random.

  • Move the source one step, and use collide_boundary to ensure that our agent stays within the box.

  • For all other agents, call interact!(other_agent, source, infection).

  • return the array agents again.

13.5 μs
complementary (generic function with 1 method)
38.0 μs
move! (generic function with 1 method)
22.2 μs
step! (generic function with 1 method)
30.0 μs

Exercise 3.2

If we call step! N times, then every agent will have made one step, on average. Let's call this one sweep of the simulation.

👉 Create a before-and-after plot of ksweeps=1000 sweeps.

  • Initialize a new vector of agents (N=50, L=40, infection is given as pandemic below).

  • Plot the state using visualize, and save the plot as a variable plot_before.

  • Run k_sweeps sweeps.

  • Plot the state again, and store as plot_after.

  • Combine the two plots into a single figure using

plot(plot_before, plot_after)
13.3 μs
pandemic
3.4 ms
1000
25.3 ms
427 ms

Exercise 3.3

Every time that you move the slider, a completely new simulation is created an run. This makes it hard to view the progress of a single simulation over time. So in this exercise, we we look at a single simulation, and plot the S, I and R curves.

👉 Plot the SIR curves of a single simulation, with the same parameters as in the previous exercise. Use k_sweep_max = 10000 as the total number of sweeps.

5.0 μs
k_sweep_max
10000
80.0 ns
563 ms

Hint

After every sweep, count the values S, I and R and push! them to 3 arrays.

68.1 ms

Exercise 3.4 (optional)

Let's make our plot come alive! There are two options to make our visualization dynamic:

👉1️⃣ Precompute one simulation run and save its intermediate states using deepcopy. You can then write an interactive visualization that shows both the state at time t (using visualize) and the history of S, I and R from time 0 up to time t. t is controlled by a slider.

👉2️⃣ Use @gif from Plots.jl to turn a sequence of plots into an animation. Be careful to skip about 50 sweeps between each animation frame, otherwise the GIF becomes too large.

This an optional exercise, and our solution to 2️⃣ is given below.

7.6 μs
18.0 s
1.3 s

Hint

let
    N = 50
    L = 40

    x = initialize(N, L)
    
    # initialize to empty arrays
    Ss, Is, Rs = Int[], Int[], Int[]
    
    Tmax = 200
    
    @gif for t in 1:Tmax
        for i in 1:50N
            step!(x, L, pandemic)
        end

        #... track S, I, R in Ss Is and Rs
        
        left = visualize(x, L)
    
        right = plot(xlim=(1,Tmax), ylim=(1,N), size=(600,300))
        plot!(right, 1:t, Ss, color=color(S), label="S")
        plot!(right, 1:t, Is, color=color(I), label="I")
        plot!(right, 1:t, Rs, color=color(R), label="R")
    
        plot(left, right)
    end
end
14.5 μs

Exercise 3.5

👉 Using L=20 and N=100, experiment with the infection and recovery probabilities until you find an epidemic outbreak. (Take the recovery probability quite small.) Modify the two infections below to match your observations.

5.4 μs
causes_outbreak
5.1 μs
does_not_cause_outbreak
6.1 μs
1.1 s
78.0 ns

Exercise 3.6

👉 With the parameters of Exercise 3.2, run 50 simulations. Plot S, I and R as a function of time for each of them (with transparency!). This should look qualitatively similar to what you saw in the previous homework. You probably need different p_infection and p_recovery values from last week. Why?

13.7 ms

Gonna wrap this into a function

4.8 μs
225 μs
simulation (generic function with 1 method)
59.7 μs
repeat_simulations (generic function with 1 method)
54.2 μs
sir_mean_error_plot (generic function with 1 method)
125 μs
100 ns
need_different_parameters_because
4.9 μs




77.0 ns

Exercise 4: Effect of socialization

In this exercise we'll modify the simple mixing model. Instead of a constant mixing probability, i.e. a constant probability that any pair of people interact on a given day, we will have a variable probability associated with each agent, modelling the fact that some people are more or less social or contagious than others.

11.0 μs

Exercise 4.1

We create a new agent type SocialAgent with fields position, status, num_infected, and social_score. The attribute social_score represents an agent's probability of interacting with any other agent in the population.

6.3 μs
1.4 ms

define the position and color methods for SocialAgent as we did for Agent. This will allow the visualize function to work. on both kinds of Agents

4.2 μs
color (generic function with 3 methods)
20.2 μs

👉 Create a function initialize_social that takes N and L, and creates N agents within a 2L x 2L box, with social_scores chosen from 10 equally-spaced between 0.1 and 0.5. (see LinRange)

3.8 μs
initialize_social (generic function with 1 method)
51.1 μs

Now that we have 2 agent types

  1. let's create an AbstractAgent type

  2. Go back in the notebook and make the agent types a subtype of AbstractAgent.

6.9 μs
5.7 μs

Exercise 4.2

Not all two agents who end up in the same grid point may actually interact in an infectious way – they may just be passing by and do not create enough exposure for communicating the disease.

👉 Write a new interact! method on SocialAgent which adds together the social_scores for two agents and uses that as the probability that they interact in a risky way. Only if they interact in a risky way, the infection is transmitted with the usual probability.

7.6 μs
interact! (generic function with 2 methods)
37.2 μs

Make sure step!, position, color, work on the type SocialAgent. If step! takes an untyped first argument, it should work for both Agent and SocialAgent types without any changes. We actually only need to specialize interact! on SocialAgent.

Exercise 4.3

👉 Plot the SIR curves of the resulting simulation.

N = 50; L = 40; number of steps = 200

In each step call step! 50N times.

8.9 μs
3.8 s

Exercise 4.4

👉 Make a scatter plot showing each agent's social_score on one axis, and the num_infected from the simulation in the other axis. Run this simulation several times and comment on the results.

5.4 μs
76.0 ns

👉 Run a simulation for 100 steps, and then apply a "lockdown" where every agent's social score gets multiplied by 0.25, and then run a second simulation which runs on that same population from there. What do you notice? How does changing this factor form 0.25 to other numbers affect things?

5.0 μs
69.0 ns

Exercise 5: (Optional) Effect of distancing

We can use a variant of the above model to investigate the effect of the mis-named "social distancing" (we want people to be socially close, but physically distant).

In this variant, we separate out the two effects "infection" and "movement": an infected agent chooses a neighbouring site, and if it finds a susceptible there then it infects it with probability pI. For simplicity we can ignore recovery.

Separately, an agent chooses a neighbouring site to move to, and moves there with probability pM if the site is vacant. (Otherwise it stays where it is.)

When pM=0, the agents cannot move, and hence are completely quarantined in their original locations.

👉 How does the disease spread in this case?

16.3 μs
79.0 ns

👉 Run the dynamics repeatedly, and plot the sites which become infected.

5.4 μs
82.0 ns

👉 How does this change as you increase the density ρ=N/(L2) of agents? Start with a small density.

This is basically the site percolation model.

When we increase pM, we allow some local motion via random walks.

11.4 μs
68.0 ns

👉 Investigate how this leaky quarantine affects the infection dynamics with different densities.

4.8 μs
69.0 ns




82.0 ns
22.9 μs

Function library

Just some helper functions used in the notebook.

11.6 μs
hint (generic function with 1 method)
43.5 μs
almost (generic function with 1 method)
19.4 μs
still_missing (generic function with 2 methods)
27.8 μs
keep_working (generic function with 2 methods)
29.9 μs
yays
18.6 ms
correct (generic function with 2 methods)
31.8 μs
not_defined (generic function with 1 method)
23.3 μs
2.6 ms